import { reactive } from 'vue'
import * as defaultCompiler from 'vue/compiler-sfc'
import { compileFile } from './transform'
import { utoa, atou } from './utils'
import {
  SFCScriptCompileOptions,
  SFCAsyncStyleCompileOptions,
  SFCTemplateCompileOptions
} from 'vue/compiler-sfc'
import { OutputModes } from './output/types'

const defaultMainFile = 'App.vue'

const welcomeCode = `
<script lang="ts">
export default {
  authorize: '/menu/template/permissionList'
}
</script>

<script lang="ts" setup>
import { ref, getCurrentInstance } from 'vue'
import { locale } from 'skrivet'
const context = getCurrentInstance()?.appContext.config.globalProperties
const $api = context.$api
const columns = [
  {
    title: '模板名称',
    dataIndex: 'templateName'
  },
  {
    title: '默认模板',
    key: 'defaultTemplate'
  },
  {
    title: '备注',
    dataIndex: 'templateRemark'
  },
  {
    title: '操作',
    width: 380,
    fixed: 'right',
    key: 'action'
  }
]
const table = ref(null)
const formValue = ref({})
const query = ref({})
const selectRowId = ref()
const addItem = (values, fullValue, dialog) => {
  $api
    .PUT('/menu/template/insert', values)
    .then((res) => {
      dialog.close()
      table.value.refresh()
      context.$message.info(res.msg)
    })
    .catch(() => {
      dialog.reset()
    })
}

const updateItem = (values, fullValue, dialog) => {
  values.id = fullValue.id
  $api
    .PUT('/menu/template/update', values)
    .then((res) => {
      dialog.close()
      table.value.refresh()
      context.$message.info(res.msg)
    })
    .catch(() => {
      dialog.reset()
    })
}

const deleteMulti = () => {
  const keys = table.value.getSelectedKeys()
  if (keys.length > 0) {
    context.$loading(true)
    $api
      .DELETE('/menu/template/deleteMulti', { ids: keys.toString() })
      .then(() => {
        context.$loading(false)
        table.value.search(query.value)
      })
      .catch(() => {
        context.$loading(false)
      })
  }
}

const deleteById = (id) => {
  context.$loading(true)
  context.$api
    .DELETE('/menu/template/delete', { id: id })
    .then(() => {
      context.$loading(false)
      table.value.search(query.value)
    })
    .catch(() => {
      context.$loading(false)
    })
}
</script>

<template>
  <a-config-provider :locale="locale">
    <section style="width: 100%; height: 100vh">
      <a-card class="body-container">
        <s-list-form>
          <template #less>
            <s-form-item size="list-middle" label="模板名称">
              <s-input v-model="query.templateName" placeholder="模板名称" />
            </s-form-item>
          </template>
          <template #more />
        </s-list-form>
        <s-tool-bar>
          <template #left>
            <s-button
              v-if="authorize.insert"
              type="primary"
              icon="plus-outlined"
              @click="
                $refs.add.init((dialog) => {
                  dialog.resetFormValue(), dialog.open()
                })
              "
              >新增</s-button
            >
            <a-popconfirm
              v-if="authorize.deleteMulti"
              title="确认删除所选数据？"
              ok-text="是"
              cancel-text="否"
              @confirm="deleteMulti"
            >
              <s-button type="danger" icon="delete-outlined">删除</s-button>
            </a-popconfirm>
          </template>
          <template #right>
            <s-button type="primary" icon="search-outlined" @click="$refs.table.search(query)"
              >查询</s-button
            >
            <s-button
              style="margin-left: 8px"
              icon="stop"
              @click="
                () => {
                  query = {}
                  $refs.table.search(query)
                }
              "
              >重置</s-button
            >
          </template>
        </s-tool-bar>
        <s-table
          ref="table"
          bordered
          :columns="columns"
          url="/menu/template/pageList"
          :scroll="{ y: tableHeight }"
        >
          <template #bodyCell="{ column, record }">
            <template v-if="column.key === 'action'">
              <a
                v-if="authorize.selectOneById"
                style="margin-right: 8px"
                @click=";(selectRowId = record.id), $refs.detail.init()"
              >
                <s-icon type="eye-outlined" />查看
              </a>
              <a
                v-if="authorize.update"
                style="margin-right: 8px"
                @click=";(selectRowId = record.id), $refs.update.init()"
              >
                <s-icon type="edit-outlined" />编辑
              </a>

              <a-popconfirm
                v-if="authorize.delete"
                title="确认删除此条数据？"
                ok-text="是"
                cancel-text="否"
                @confirm="deleteById(record.id)"
              >
                <a style="margin-right: 8px"> <s-icon type="delete-outlined" />删除 </a>
              </a-popconfirm>
            </template>
            <template v-else-if="column.key == 'defaultTemplate'">
              <div v-if="record.defaultTemplate == '1'">是</div>
              <div v-else>否</div>
            </template></template
          >
        </s-table>

        <s-dynamic-form-dialog
          ref="add"
          title="添加"
          form-id="5014f2dd5aee4b50bce04bc79a459749"
          @save="addItem"
        ></s-dynamic-form-dialog>
        <s-dynamic-form-dialog
          ref="detail"
          disabled
          :pull-form-data="
            () => {
              return $api.GET('/menu/template/selectOneById', { id: selectRowId })
            }
          "
          form-id="5014f2dd5aee4b50bce04bc79a459749"
        >
        </s-dynamic-form-dialog>
        <s-dynamic-form-dialog
          ref="update"
          title="编辑"
          :pull-form-data="
            () => {
              return $api.GET('/menu/template/selectOneById', { id: selectRowId })
            }
          "
          form-id="5014f2dd5aee4b50bce04bc79a459749"
          @save="updateItem"
        >
        </s-dynamic-form-dialog>
      </a-card>
    </section>
  </a-config-provider>
</template>

`.trim()

export class File {
  filename: string
  code: string
  hidden: boolean
  compiled = {
    js: '',
    css: '',
    ssr: ''
  }

  constructor(filename: string, code = '', hidden = false) {
    this.filename = filename
    this.code = code
    this.hidden = hidden
  }
}

export interface StoreState {
  mainFile: string
  files: Record<string, File>
  activeFile: File
  errors: (string | Error)[]
  vueRuntimeURL: string
  vueServerRendererURL: string
  // used to force reset the sandbox
  resetFlip: boolean
}

export interface SFCOptions {
  script?: Partial<SFCScriptCompileOptions>
  style?: Partial<SFCAsyncStyleCompileOptions>
  template?: Partial<SFCTemplateCompileOptions>
}

export interface Store {
  state: StoreState
  options?: SFCOptions
  compiler: typeof defaultCompiler
  vueVersion?: string
  init: () => void
  setActive: (filename: string) => void
  addFile: (filename: string | File) => void
  deleteFile: (filename: string) => void
  getImportMap: () => any
  initialShowOutput: boolean
  initialOutputMode: OutputModes
}

export interface StoreOptions {
  serializedState?: string
  showOutput?: boolean
  // loose type to allow getting from the URL without inducing a typing error
  outputMode?: OutputModes | string
  defaultVueRuntimeURL?: string
  defaultVueServerRendererURL?: string
  defaultVueXURL?: string
  defaultSkrivetURL?: string
  defaultAntdURL?: string
  defaultDayjsURL?: string
  defaultAntdIconURL?: string
}

export class ReplStore implements Store {
  state: StoreState
  compiler = defaultCompiler
  vueVersion?: string
  options?: SFCOptions
  initialShowOutput: boolean
  initialOutputMode: OutputModes

  private defaultVueRuntimeURL: string
  private defaultVueServerRendererURL: string
  private defaultVueXURL: string
  private defaultSkrivetURL: string
  private defaultAntdURL: string
  private defaultAntdIconURL: string
  private defaultDayjsURL: string
  private pendingCompiler: Promise<any> | null = null

  constructor({
    serializedState = '',
    defaultVueRuntimeURL = `/lib/vue.runtime.esm-browser.js`,
    defaultVueServerRendererURL = `/lib/server-renderer.esm-browser.js`,
    defaultVueXURL = '/lib/vuex.esm-browser.prod.js',
    defaultSkrivetURL = '/lib/skrivet.js',
    defaultAntdURL = '/lib/antd.js',
    defaultAntdIconURL = 'lib/antd-icons.js',
    defaultDayjsURL = 'lib/dayjs.js',
    showOutput = false,
    outputMode = 'preview'
  }: StoreOptions = {}) {
    let files: StoreState['files'] = {}

    if (serializedState) {
      const saved = JSON.parse(atou(serializedState))
      for (const filename in saved) {
        files[filename] = new File(filename, saved[filename])
      }
    } else {
      files = {
        [defaultMainFile]: new File(defaultMainFile, welcomeCode)
      }
    }

    this.defaultVueRuntimeURL = defaultVueRuntimeURL
    this.defaultVueServerRendererURL = defaultVueServerRendererURL
    this.defaultVueXURL = defaultVueXURL
    this.defaultSkrivetURL = defaultSkrivetURL
    this.defaultAntdURL = defaultAntdURL
    this.defaultAntdIconURL = defaultAntdIconURL
    this.defaultDayjsURL = defaultDayjsURL
    this.initialShowOutput = showOutput
    this.initialOutputMode = outputMode as OutputModes
    let mainFile = defaultMainFile
    if (!files[mainFile]) {
      mainFile = Object.keys(files)[0]
    }
    this.state = reactive({
      mainFile,
      files,
      activeFile: files[mainFile],
      errors: [],
      vueRuntimeURL: this.defaultVueRuntimeURL,
      vueServerRendererURL: this.defaultVueServerRendererURL,
      vueXURL: this.defaultVueXURL,
      skrivetURL: this.defaultSkrivetURL,
      antdURL: this.defaultAntdURL,
      antdIconsURL: this.defaultAntdIconURL,
      dayjsURL: this.defaultDayjsURL,
      resetFlip: true
    })

    this.initImportMap()
  }

  // don't start compiling until the options are set
  init() {
    // watchEffect(() => {
    //   compileFile(this, this.state.activeFile)
    // })
    compileFile(this, this.state.activeFile)
    for (const file in this.state.files) {
      if (file !== defaultMainFile) {
        compileFile(this, this.state.files[file])
      }
    }
  }
  compileAllFile() {
    for (const file in this.state.files) {
      if (file !== defaultMainFile) {
        compileFile(this, this.state.files[file])
        if (this.state.errors.length > 0) {
          return
        }
      }
    }
    compileFile(this, this.state.activeFile)
  }

  setActive(filename: string) {
    this.state.activeFile = this.state.files[filename]
  }

  addFile(fileOrFilename: string | File): void {
    const file = typeof fileOrFilename === 'string' ? new File(fileOrFilename) : fileOrFilename
    this.state.files[file.filename] = file
    if (!file.hidden) this.setActive(file.filename)
  }

  deleteFile(filename: string) {
    const that = this
    if (that.state.activeFile.filename === filename) {
      that.state.activeFile = that.state.files[that.state.mainFile]
    }
    delete that.state.files[filename]
  }

  serialize() {
    const files = this.getFiles()
    const importMap = files['import-map.json']
    if (importMap) {
      const { imports } = JSON.parse(importMap)
      if (imports['vue'] === this.defaultVueRuntimeURL) {
        delete imports['vue']
      }
      if (imports['vue/server-renderer'] === this.defaultVueServerRendererURL) {
        delete imports['vue/server-renderer']
      }
      if (!Object.keys(imports).length) {
        delete files['import-map.json']
      } else {
        files['import-map.json'] = JSON.stringify({ imports }, null, 2)
      }
    }
    return '#' + utoa(JSON.stringify(files))
  }

  getFiles() {
    const exported: Record<string, string> = {}
    for (const filename in this.state.files) {
      exported[filename] = this.state.files[filename].code
    }
    return exported
  }

  async setFiles(newFiles: Record<string, string>, mainFile = defaultMainFile) {
    const files: Record<string, File> = {}
    if (mainFile === defaultMainFile && !newFiles[mainFile]) {
      files[mainFile] = new File(mainFile, welcomeCode)
    }
    for (const filename in newFiles) {
      files[filename] = new File(filename, newFiles[filename])
    }
    for (const file in files) {
      await compileFile(this, files[file])
    }
    this.state.mainFile = mainFile
    this.state.files = files
    this.initImportMap()
    this.setActive(mainFile)
    this.forceSandboxReset()
  }

  private forceSandboxReset() {
    this.state.resetFlip = !this.state.resetFlip
  }

  private initImportMap() {
    const map = this.state.files['import-map.json']
    if (!map) {
      this.state.files['import-map.json'] = new File(
        'import-map.json',
        JSON.stringify(
          {
            imports: {
              vue: this.defaultVueRuntimeURL,
              'vue/server-renderer': this.defaultVueServerRendererURL,
              vuex: this.defaultVueXURL,
              skrivet: this.defaultSkrivetURL,
              'ant-design-vue': this.defaultAntdURL,
              dayjs: this.defaultDayjsURL,
              '@ant-design/icons-vue': this.defaultAntdIconURL
            }
          },
          null,
          2
        )
      )
    } else {
      try {
        const json = JSON.parse(map.code)
        if (!json.imports.vue) {
          json.imports.vue = this.defaultVueRuntimeURL
        } else {
          json.imports.vue = fixURL(json.imports.vue)
        }
        if (!json.imports.vuex) {
          json.imports.vuex = this.defaultVueXURL
        } else {
          json.imports.vuex = fixURL(json.imports.defaultVueXURL)
        }
        if (!json.imports.skrivet) {
          json.imports.skrivet = this.defaultSkrivetURL
        } else {
          json.imports.skrivet = fixURL(json.imports.defaultSkrivetURL)
        }
        if (!json.imports.dayjs) {
          json.imports.dayjs = this.defaultDayjsURL
        } else {
          json.imports.dayjs = fixURL(json.imports.defaultDayjsURL)
        }
        if (!json.imports['vue/server-renderer']) {
          json.imports['vue/server-renderer'] = this.defaultVueServerRendererURL
        } else {
          json.imports['vue/server-renderer'] = fixURL(json.imports['vue/server-renderer'])
        }
        if (!json.imports['ant-design-vue']) {
          json.imports['ant-design-vue'] = this.defaultAntdURL
        } else {
          json.imports['ant-design-vue'] = fixURL(json.imports['ant-design-vue'])
        }
        if (!json.imports['@ant-design/icons-vue']) {
          json.imports['@ant-design/icons-vue'] = this.defaultAntdIconURL
        } else {
          json.imports['@ant-design/icons-vue'] = fixURL(json.imports['@ant-design/icons-vue'])
        }
        map.code = JSON.stringify(json, null, 2)
      } catch (e) {}
    }
  }

  getImportMap() {
    try {
      return JSON.parse(this.state.files['import-map.json'].code)
    } catch (e) {
      this.state.errors = [`Syntax error in import-map.json: ${(e as Error).message}`]
      return {}
    }
  }

  setImportMap(map: {
    imports: Record<string, string>
    scopes?: Record<string, Record<string, string>>
  }) {
    this.state.files['import-map.json']!.code = JSON.stringify(map, null, 2)
  }
}

function fixURL(url: string) {
  return url.replace('https://sfc.vuejs', 'https://play.vuejs')
}
